/*
 * Copyright (C) 2008-2009 the original author or authors
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *         http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.code.liquidform;

import com.google.code.liquidform.internal.AliasFactory;
import com.google.code.liquidform.internal.FrameworkObjectFactory;
import com.google.code.liquidform.internal.managedobjects.FrameworkManagedObject;

/**
 * Provides support for arithmetic expressions. The method names here should be
 * self explanatory. Varargs are used to enable operations with multiple
 * operands.
 * 
 * <p>
 * The return type of those methods extends {@link Number}. The JSR states that
 * <i>"Arithmetic operations use numeric promotion"</i> but this
 * implementation, based on generics, falls back to the first common superclass
 * (namely {@link Number}) as soon as the arguments are not all of the exact
 * same type.
 * 
 * Note that with {@link SubQuery}, the return type is necessarily
 * {@link Number}.
 * </p>
 */
public class Arithmetics {

	private static final String UNARY_PLUS = "+ ";
	private static final String UNARY_MINUS = "- ";
	private static final String PLUS = " + ";
	private static final String MINUS = " - ";
	private static final String TIMES = " * ";
	private static final String DIVIDE = " / ";

	private Arithmetics() {
		// utility class
	}

	// grouping with parentheses
	public static <N extends Number> N paren(final N arithmeticExpression) {
		N grouppedExpression = FrameworkObjectFactory.INSTANCE
				.makeNew(arithmeticExpression);

		AliasFactory.remember(grouppedExpression, new FrameworkManagedObject() {
			@Override
			public String getTargetRepresentation() {
				return "(" + AliasFactory.safeStringForm(arithmeticExpression)
						+ ")";
			}
		});

		return grouppedExpression;
	}

	// addition

	public static <N extends Number> N plus(final N operand,
			final N... moreOperands) {
		N sum = FrameworkObjectFactory.INSTANCE.makeNew(operand);

		AliasFactory.remember(sum, new FrameworkManagedObject() {
			@Override
			public String getTargetRepresentation() {

				if (moreOperands.length == 0) {
					return UNARY_PLUS + AliasFactory.safeStringForm(operand);
				}

				return buildChainedOps(PLUS, operand, moreOperands);
			}
		});

		return sum;
	}

	public static Number plus(final SubQuery<? extends Number> subQuery,
			Number operand) {
		return plus(rememberAsDouble(subQuery), operand);
	}

	public static Number plus(final SubQuery<? extends Number> subQuery,
			final SubQuery<? extends Number>... moreSubQueries) {
		Double subQueryAsNumber = rememberAsDouble(subQuery);

		if (moreSubQueries.length == 0) {
			return plus(subQueryAsNumber);
		}

		return plus(subQueryAsNumber,
				rememberAllSubQueriesAsNumbers(moreSubQueries));
	}

	// subtraction

	public static <N extends Number> N minus(final N operand,
			final N... moreOperands) {
		N difference = FrameworkObjectFactory.INSTANCE.makeNew(operand);

		AliasFactory.remember(difference, new FrameworkManagedObject() {
			@Override
			public String getTargetRepresentation() {

				if (moreOperands.length == 0) {
					return UNARY_MINUS + AliasFactory.safeStringForm(operand);
				}

				return buildChainedOps(MINUS, operand, moreOperands);
			}
		});

		return difference;
	}

	public static Number minus(final SubQuery<? extends Number> subQuery,
			Number operand) {
		return minus(rememberAsDouble(subQuery), operand);
	}

	public static Number minus(Number operand,
			final SubQuery<? extends Number> subQuery) {
		return minus(operand, rememberAsDouble(subQuery));
	}

	public static Number minus(final SubQuery<? extends Number> subQuery,
			SubQuery<? extends Number>... moreSubQueries) {
		Double subQueryAsNumber = rememberAsDouble(subQuery);

		if (moreSubQueries.length == 0) {
			return minus(subQueryAsNumber);
		}

		return minus(subQueryAsNumber,
				rememberAllSubQueriesAsNumbers(moreSubQueries));
	}

	// product

	public static <N extends Number> N times(final N factor1, final N factor2,
			final N... moreFactors) {
		N product = FrameworkObjectFactory.INSTANCE.makeNew(factor1);

		AliasFactory.remember(product, new FrameworkManagedObject() {
			@Override
			public String getTargetRepresentation() {

				if (moreFactors.length == 0) {
					return AliasFactory.safeStringForm(factor1) + TIMES
							+ AliasFactory.safeStringForm(factor2);
				}

				return AliasFactory.safeStringForm(factor1) + TIMES
						+ buildChainedOps(TIMES, factor2, moreFactors);
			}
		});

		return product;
	}

	public static Number times(final SubQuery<? extends Number> subQuery,
			Number factor) {
		return times(rememberAsDouble(subQuery), factor);
	}

	public static Number times(final SubQuery<? extends Number> subQuery1,
			final SubQuery<? extends Number> subQuery2,
			SubQuery<? extends Number>... moreSubQueries) {
		Double subQuery1AsNumber = rememberAsDouble(subQuery1);

		if (moreSubQueries.length == 0) {
			return times(subQuery1AsNumber, rememberAsDouble(subQuery2));
		}

		return times(subQuery1AsNumber, rememberAsDouble(subQuery2),
				rememberAllSubQueriesAsNumbers(moreSubQueries));
	}

	// division

	public static <N extends Number> N divide(final N dividend, final N divisor) {
		N quotient = FrameworkObjectFactory.INSTANCE.makeNew(dividend);

		AliasFactory.remember(quotient, new FrameworkManagedObject() {
			@Override
			public String getTargetRepresentation() {

				return AliasFactory.safeStringForm(dividend) + DIVIDE
						+ AliasFactory.safeStringForm(divisor);
			}
		});

		return quotient;
	}

	public static Number divide(
			final SubQuery<? extends Number> subQueryDividend, Number divisor) {
		return divide(rememberAsDouble(subQueryDividend), divisor);
	}

	public static Number divide(Number dividend,
			final SubQuery<? extends Number> subQueryDivisor) {
		return divide(dividend, rememberAsDouble(subQueryDivisor));
	}

	public static Number divide(
			final SubQuery<? extends Number> subQueryDividend,
			final SubQuery<? extends Number> subQueryDivisor) {
		return divide(rememberAsDouble(subQueryDividend),
				rememberAsDouble(subQueryDivisor));
	}

	private static <O extends Object> String buildChainedOps(String operator,
			O operand, O... moreOperands) {
		StringBuilder sb = new StringBuilder();
		sb.append(AliasFactory.safeStringForm(operand));
		for (O element : moreOperands) {
			sb.append(operator).append(AliasFactory.safeStringForm(element));
		}
		return sb.toString();
	};

	private static Double rememberAsDouble(
			final SubQuery<? extends Number> subQuery) {
		Double subQueryAsNumber = FrameworkObjectFactory.INSTANCE
				.makeNew(Double.class);

		AliasFactory.remember(subQueryAsNumber, new FrameworkManagedObject() {
			@Override
			public String getTargetRepresentation() {
				return AliasFactory.safeStringForm(subQuery);
			}
		});
		return subQueryAsNumber;
	}

	private static Number[] rememberAllSubQueriesAsNumbers(
			SubQuery<? extends Number>... moreSubQueries) {
		Number[] result = new Number[moreSubQueries.length];
		for (int i = 0; i < moreSubQueries.length; i++) {
			result[i] = rememberAsDouble(moreSubQueries[i]);
		}
		return result;
	};

}
